package org.koin.core.module;

import org.koin.core.definition.BeanDefinition;
import org.koin.core.definition.Definition;
import org.koin.core.definition.Definitions;
import org.koin.core.definition.Options;
import org.koin.core.error.DefinitionOverrideException;
import org.koin.core.qualifier.Qualifier;
import org.koin.core.qualifier.TypeQualifier;
import org.koin.core.scope.ScopeDefinition;
import org.koin.dsl.ScopeDSL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

/**
 * Koin Module
 * Gather/help compose Koin definitions
 *
 * @author Arnaud Giuliani
 */
public class Module {

    private boolean createAtStart;
    private boolean override;
    private Qualifier rootScope = ScopeDefinition.ROOT_SCOPE_QUALIFIER;
    private boolean isLoaded = false;
    private List<Qualifier> scopes = new ArrayList<>();
    private HashSet<BeanDefinition> definitions = new HashSet<>();

    public Module(boolean createAtStart, boolean override) {
        this.createAtStart = createAtStart;
        this.override = override;
    }

    public static void addDefinition(HashSet<BeanDefinition> set,
                                     BeanDefinition bean)
            throws DefinitionOverrideException {
        boolean added = set.add(bean);
        if (!added && !bean.getOptions().isOverride()) {
            throw new DefinitionOverrideException(
                    "Definition '" + bean + "' try to override existing definition. Please use override option to fix it"
            );
        } else if (!added && bean.getOptions().isOverride()) {
            set.remove(bean);
            set.add(bean);
        }
    }

    public boolean getCreateAtStart() {
        return createAtStart;
    }

    public boolean getOverride() {
        return override;
    }

    /**
     * Declare a group a scoped definition with a given scope qualifier
     *
     * @param qualifier
     */
    public void scope(Qualifier qualifier, ScopeSet scopeSet) {
        ScopeDSL scopeDSL = new ScopeDSL(qualifier, definitions);
        scopeSet.invoke(scopeDSL);
        scopes.add(qualifier);
    }

    /**
     * Class Typed Scope
     */
    public <T> void scope(Class<T> clazz, ScopeSet scopeSet) {
        TypeQualifier qualifier = new TypeQualifier(clazz);
        ScopeDSL scopeDSL = new ScopeDSL(qualifier, definitions);
        scopeSet.invoke(scopeDSL);
        scopes.add(qualifier);
    }

    /**
     * Declare a Single definition
     *
     * @param qualifier
     * @param createdAtStart
     * @param override
     * @param definition     - definition function
     */
    public <T> BeanDefinition<T> single(
            Class<T> clazz,
            Qualifier qualifier,
            boolean createdAtStart,
            boolean override,
            Definition<T> definition
    ) throws DefinitionOverrideException {
        Options options = makeOptions(override, createdAtStart);
        BeanDefinition def = Definitions.INSTANCE()
                .createSingle(clazz,
                        qualifier,
                        definition,
                        options,
                        new ArrayList<>(),
                        rootScope);
        addDefinition(definitions, def);
        return def;
    }

    /**
     * Declare a Single definition
     *
     * @param qualifier
     * @param definition - definition function
     */
    public <T> BeanDefinition<T> single(
            Class<T> clazz,
            Qualifier qualifier,
            Definition<T> definition
    ) throws DefinitionOverrideException {

        return this.single(clazz, qualifier, false, false, definition);
    }

    public <T> BeanDefinition<T> single(Class<T> clazz,
                                        Definition<T> definition)
            throws DefinitionOverrideException {
        return this.single(clazz, null, false, false, definition);
    }

    public Options makeOptions(boolean override, boolean createdAtStart) {
        return new Options()
                .setCreatedAtStart(this.createAtStart || createdAtStart)
                .setOverride(this.override || override);
    }

    /**
     * Declare a Factory definition
     *
     * @param qualifier
     * @param override
     * @param definition - definition function
     */
    public <T> BeanDefinition<T> factory(
            Class<T> clazz,
            Qualifier qualifier,
            boolean override,
            Definition<T> definition
    ) throws DefinitionOverrideException {
        Options options = makeOptions(override, false);
        BeanDefinition def = Definitions
                .INSTANCE()
                .createFactory(
                        clazz,
                        qualifier,
                        definition,
                        options,
                        new ArrayList<>(),
                        rootScope);
        addDefinition(definitions, def);
        return def;
    }

    /**
     * Declare a Factory definition
     *
     * @param definition - definition function
     */
    public <T> BeanDefinition<T> factory(
            Class<T> clazz,
            Definition<T> definition
    ) throws DefinitionOverrideException {

        return this.factory(clazz, null, false, definition);
    }

    public List<Module> plus(Module module) {
        List<Module> list = new ArrayList<>();
        list.add(this);
        list.add(module);
        return list;
    }

    public List<Module> plus(List<Module> modules) {
        List<Module> result = Arrays.asList(this);
        result.addAll(modules);
        return result;
    }

    public Qualifier getRootScope() {
        return rootScope;
    }

    public boolean isLoaded() {
        return isLoaded;
    }

    public void setLoaded(boolean loaded) {
        isLoaded = loaded;
    }

    public List<Qualifier> getScopes() {
        return scopes;
    }

    public HashSet<BeanDefinition> getDefinitions() {
        return definitions;
    }

    public interface ScopeSet {
        void invoke(ScopeDSL scopeDSL);
    }
}
